﻿namespace EasyWeb.Data.EntityFramework
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.ComponentModel.Composition;
    using System.Linq;
    using System.Threading.Tasks;
    using System.Transactions;
    using EasyWeb.Data.EntityFramework.Models;
    using EasyWeb.Extensions;

    [Export(typeof(IEWFieldDefinitionDao))]
    [PartCreationPolicy(CreationPolicy.Shared)]
    public class EWFieldDefinitionDao : EWDao, IEWFieldDefinitionDao
    {
        public readonly EWEntityDefinitionDao entityDefinitionDao = new EWEntityDefinitionDao();

        [Import(typeof(IEWDdlSqlGenerator))]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public IEWDdlSqlGenerator ddlSqlGenerator;

        public EWFieldDefinitionDao()
        {
            EWExtensionManager.CompositionContainer.SatisfyImportsOnce(this);
        }

        public virtual async Task<IEnumerable<EWFieldDefinition>> SelectAsync()
        {
            using (var transaction = EWHelpers.Data.BeginTransaction())
            {
                var result = await SelectCoreAsync<EWFieldDefinition, EWFieldDefinitionModel>();

                transaction.Complete();

                return result;
            }
        }

        public virtual async Task CreateAsync(EWFieldDefinition domain)
        {
            EWGuard.IsNotNull(domain, "domain");

            using (var transaction = EWHelpers.Data.BeginTransaction())
            {
                var entity = await this.entityDefinitionDao.SelectEntityAsync(domain.EntityDefinitionId);

                await ValidateCreateAsync(entity, domain);

                await CreateCoreAsync<EWFieldDefinition, EWFieldDefinitionModel>(domain);
                await CreateColumnAsync(entity, domain);

                transaction.Complete();
            }
        }

        public virtual async Task UpdateAsync(EWFieldDefinition domain)
        {
            EWGuard.IsNotNull(domain, "domain");

            using (var transaction = EWHelpers.Data.BeginTransaction())
            {
                var entity = await this.entityDefinitionDao.SelectEntityAsync(domain.EntityDefinitionId);

                await ValidateUpdateAsync(entity, domain);

                var oldDomain = await SelectFieldAsync(domain.Id);
                await UpdateCoreAsync<EWFieldDefinition, EWFieldDefinitionModel>(domain);
                await UpdateColumnAsync(entity, oldDomain, domain);

                transaction.Complete();
            }
        }

        public virtual async Task DeleteAsync(Guid id, byte[] timestamp)
        {
            EWGuard.IsNotNull(timestamp, "timestamp");

            using (var transaction = EWHelpers.Data.BeginTransaction())
            {
                var domain = await SelectFieldAsync(id);
                var entity = await this.entityDefinitionDao.SelectEntityAsync(domain.EntityDefinitionId);

                await ValidateDeleteAsync(entity, domain);

                await DeleteCoreAsync<EWFieldDefinitionModel>(id, timestamp);
                await DeleteColumnAsync(entity, domain);

                transaction.Complete();
            }
        }

        internal async Task ValidateCreateAsync(EWEntityDefinition entity, EWFieldDefinition field)
        {
            await ValidateInternalNameAsync(field);
            await ValidateColumnsAsync(entity, null);
        }

        internal async Task ValidateUpdateAsync(EWEntityDefinition entity, EWFieldDefinition field)
        {
            await ValidateInternalNameAsync(field);
            await ValidateColumnsAsync(entity, field);
        }

        internal async Task ValidateDeleteAsync(EWEntityDefinition entity, EWFieldDefinition field)
        {
            var existing = await SelectExistingColumnsAsync(entity, null);

            var columns = field.CreateFieldDefinitionColumns();
            foreach (var column in columns)
            {
                if (!existing.Any(e => StringComparer.OrdinalIgnoreCase.Compare(e.Name, column.Name) == 0))
                {
                    throw new InvalidOperationException(string.Format("The field with name '{0}' does not exist.", column.Name));
                }
            }
        }

        internal async Task ValidateInternalNameAsync(EWFieldDefinition field)
        {
            var all = (await SelectAsync()).Where(f => f.EntityDefinitionId == field.EntityDefinitionId && f.Id != field.Id);
            if (all.Any(f => StringComparer.OrdinalIgnoreCase.Compare(f.InternalName, field.InternalName) == 0))
            {
                throw new EWDataException(string.Format("The field with name '{0}' already exists.", field.InternalName));
            }
        }

        internal async Task ValidateColumnsAsync(EWEntityDefinition entity, EWFieldDefinition field)
        {
            var existing = await SelectExistingColumnsAsync(entity, field);

            if (field != null)
            {
                var columns = field.CreateFieldDefinitionColumns();
                foreach (var column in columns)
                {
                    if (existing.Any(e => StringComparer.OrdinalIgnoreCase.Compare(e.Name, column.Name) == 0))
                    {
                        throw new InvalidOperationException(string.Format("The field with name '{0}' already exist.", column.Name));
                    }
                }
            }
        }

        internal async Task<List<EWFieldDefinitionColumn>> SelectExistingColumnsAsync(EWEntityDefinition entity, EWFieldDefinition field)
        {
            var result = new List<EWFieldDefinitionColumn>();

            var fields = (await SelectAsync()).Where(f => f.EntityDefinitionId == entity.Id);
            if (field != null)
            {
                fields = fields.Where(f => f.Id != field.Id);
            }

            foreach (var f in fields)
            {
                result.AddRange(f.CreateFieldDefinitionColumns());
            }

            return result;
        }

        internal async Task<EWFieldDefinition> SelectFieldAsync(Guid id)
        {
            var result = (await SelectAsync()).FirstOrDefault(d => d.Id == id);
            if (result == null)
            {
                throw new EWDataConcurrencyException(string.Format("The field with ID '{0}' does not exist.", id));
            }

            return result;
        }

        internal async Task CreateColumnAsync(EWEntityDefinition entity, EWFieldDefinition field)
        {
            await Execute(async db =>
            {
                var tableName = await EWCommon.GetDatabaseUserTableNameAsync(entity.InternalName);
                var columns = field.CreateFieldDefinitionColumns();
                foreach (var column in columns)
                {
                    var columnName = await EWCommon.GetDatabaseUserColumnNameAsync(column.Name);
                    var sql = this.ddlSqlGenerator.CreateColumn(tableName, columnName, column.Type, true);
                    await db.Database.ExecuteSqlCommandAsync(sql);
                }

                return default(object);
            });
        }

        internal async Task UpdateColumnAsync(EWEntityDefinition entity, EWFieldDefinition oldField, EWFieldDefinition newField)
        {
            await Execute(async db =>
            {
                var tableName = await EWCommon.GetDatabaseUserTableNameAsync(entity.InternalName);
                var oldColumns = oldField.CreateFieldDefinitionColumns();
                var newColumns = newField.CreateFieldDefinitionColumns();
                for (int i = 0; i < oldColumns.Count; i++)
                {
                    // HACK: we assume that the count of old and new columns are equal.

                    var oldColumn = oldColumns[i];
                    var newColumn = newColumns[i];
                    var type = CompareColumns(oldColumn, newColumn);

                    if ((type & ColumnChangeType.Name) == ColumnChangeType.Name)
                    {
                        var oldColumnName = await EWCommon.GetDatabaseUserColumnNameAsync(oldColumn.Name);
                        var newColumnName = await EWCommon.GetDatabaseUserColumnNameAsync(newColumn.Name);
                        var sql = this.ddlSqlGenerator.ChangeColumnName(tableName, oldColumnName, newColumnName);
                        await db.Database.ExecuteSqlCommandAsync(sql);
                    }

                    if ((type & ColumnChangeType.Type) == ColumnChangeType.Type)
                    {
                        var newColumnName = await EWCommon.GetDatabaseUserColumnNameAsync(newColumn.Name);
                        var sql = this.ddlSqlGenerator.ChangeColumnType(tableName, newColumnName, newColumn.Type, true);
                        await db.Database.ExecuteSqlCommandAsync(sql);
                    }
                }

                return default(object);
            });
        }

        internal async Task DeleteColumnAsync(EWEntityDefinition entity, EWFieldDefinition field)
        {
            await Execute(async db =>
            {
                var tableName = await EWCommon.GetDatabaseUserTableNameAsync(entity.InternalName);
                var columns = field.CreateFieldDefinitionColumns();
                foreach (var column in columns)
                {
                    var columnName = await EWCommon.GetDatabaseUserColumnNameAsync(column.Name);
                    var sql = this.ddlSqlGenerator.DeleteColumn(tableName, columnName);
                    await db.Database.ExecuteSqlCommandAsync(sql);
                }

                return default(object);
            });
        }

        internal static ColumnChangeType CompareColumns(EWFieldDefinitionColumn oldColumn, EWFieldDefinitionColumn newColumn)
        {
            var result = ColumnChangeType.None;

            if (StringComparer.OrdinalIgnoreCase.Compare(oldColumn.Name, newColumn.Name) != 0)
            {
                result |= ColumnChangeType.Name;
            }

            if (oldColumn.Type != newColumn.Type)
            {
                result |= ColumnChangeType.Type;
            }

            return result;
        }

        [Flags]
        internal enum ColumnChangeType : byte
        {
            None = 0,
            Name = 1,
            Type = 2,
        }
    }
}
